// Copyright (c) 2019-present, iQIYI, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

// Created by caikelun on 2019-09-03.
package xcrash;

import static android.os.FileObserver.CLOSE_WRITE;

import android.content.Context;
import android.os.Build;
import android.os.FileObserver;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings("StaticFieldLeak")
class AnrHandler {

  private static final AnrHandler instance = new AnrHandler();

  private final Date startTime = new Date();
  private final Pattern patPidTime = Pattern.compile("^-----\\spid\\s(\\d+)\\sat\\s(.*)\\s-----$");
  private final Pattern patProcessName = Pattern.compile("^Cmd\\sline:\\s+(.*)$");
  private final long anrTimeoutMs = 15 * 1000;

  private Context ctx;
  private int pid;
  private String processName;
  private String appId;
  private String appVersion;
  private String logDir;
  private boolean checkProcessState;
  private int logcatSystemLines;
  private int logcatEventsLines;
  private int logcatMainLines;
  private boolean dumpFds;
  private boolean dumpNetworkInfo;
  private ICrashCallback callback;
  private long lastTime = 0;
  private FileObserver fileObserver = null;

  private AnrHandler() {}

  static AnrHandler getInstance() {
    return instance;
  }

  @SuppressWarnings("deprecation")
  void initialize(
      Context ctx,
      int pid,
      String processName,
      String appId,
      String appVersion,
      String logDir,
      boolean checkProcessState,
      int logcatSystemLines,
      int logcatEventsLines,
      int logcatMainLines,
      boolean dumpFds,
      boolean dumpNetworkInfo,
      ICrashCallback callback) {

    // check API level
    if (Build.VERSION.SDK_INT >= 21) {
      return;
    }

    this.ctx = ctx;
    this.pid = pid;
    this.processName = (TextUtils.isEmpty(processName) ? "unknown" : processName);
    this.appId = appId;
    this.appVersion = appVersion;
    this.logDir = logDir;
    this.checkProcessState = checkProcessState;
    this.logcatSystemLines = logcatSystemLines;
    this.logcatEventsLines = logcatEventsLines;
    this.logcatMainLines = logcatMainLines;
    this.dumpFds = dumpFds;
    this.dumpNetworkInfo = dumpNetworkInfo;
    this.callback = callback;

    fileObserver =
        new FileObserver("/data/anr/", CLOSE_WRITE) {
          public void onEvent(int event, String path) {
            try {
              if (path != null) {
                String filepath = "/data/anr/" + path;
                if (filepath.contains("trace")) {
                  handleAnr(filepath);
                }
              }
            } catch (Exception e) {
              XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver onEvent failed", e);
            }
          }
        };

    try {
      fileObserver.startWatching();
    } catch (Exception e) {
      fileObserver = null;
      XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver startWatching failed", e);
    }
  }

  void notifyJavaCrashed() {
    if (fileObserver != null) {
      try {
        fileObserver.stopWatching();
      } catch (Exception e) {
        XCrash.getLogger().e(Util.TAG, "AnrHandler fileObserver stopWatching failed", e);
      } finally {
        fileObserver = null;
      }
    }
  }

  private void handleAnr(String filepath) {
    Date anrTime = new Date();

    // check ANR time interval
    if (anrTime.getTime() - lastTime < anrTimeoutMs) {
      return;
    }

    // check process error state
    if (this.checkProcessState) {
      if (!Util.checkProcessAnrState(this.ctx, anrTimeoutMs)) {
        return;
      }
    }

    // get trace
    String trace = getTrace(filepath, anrTime.getTime());
    if (TextUtils.isEmpty(trace)) {
      return;
    }

    // captured ANR
    lastTime = anrTime.getTime();

    // delete extra ANR log files
    if (!FileManager.getInstance().maintainAnr()) {
      return;
    }

    // get emergency
    String emergency = null;
    try {
      emergency = getEmergency(anrTime, trace);
    } catch (Exception e) {
      XCrash.getLogger().e(Util.TAG, "AnrHandler getEmergency failed", e);
    }

    // create log file
    File logFile = null;
    try {
      String logPath =
          String.format(
              Locale.US,
              "%s/%s_%020d_%s__%s%s",
              logDir,
              Util.logPrefix,
              anrTime.getTime() * 1000,
              appVersion,
              processName,
              Util.anrLogSuffix);
      logFile = FileManager.getInstance().createLogFile(logPath);
    } catch (Exception e) {
      XCrash.getLogger().e(Util.TAG, "AnrHandler createLogFile failed", e);
    }

    // write info to log file
    if (logFile != null) {
      RandomAccessFile raf = null;
      try {
        raf = new RandomAccessFile(logFile, "rws");

        // write emergency info
        if (emergency != null) {
          raf.write(emergency.getBytes("UTF-8"));
        }

        // If we wrote the emergency info successfully, we don't need to return it from callback
        // again.
        emergency = null;

        // write logcat
        if (logcatMainLines > 0 || logcatSystemLines > 0 || logcatEventsLines > 0) {
          raf.write(
              Util.getLogcat(logcatMainLines, logcatSystemLines, logcatEventsLines)
                  .getBytes("UTF-8"));
        }

        // write fds
        if (dumpFds) {
          raf.write(Util.getFds().getBytes("UTF-8"));
        }

        // write network info
        if (dumpNetworkInfo) {
          raf.write(Util.getNetworkInfo().getBytes("UTF-8"));
        }

        // write memory info
        raf.write(Util.getMemoryInfo().getBytes("UTF-8"));
      } catch (Exception e) {
        XCrash.getLogger().e(Util.TAG, "AnrHandler write log file failed", e);
      } finally {
        if (raf != null) {
          try {
            raf.close();
          } catch (Exception ignored) {
          }
        }
      }
    }

    // callback
    if (callback != null) {
      try {
        callback.onCrash(logFile == null ? null : logFile.getAbsolutePath(), emergency);
      } catch (Exception ignored) {
      }
    }
  }

  private String getEmergency(Date anrTime, String trace) {
    return Util.getLogHeader(startTime, anrTime, Util.anrCrashType, appId, appVersion)
        + "pid: "
        + pid
        + "  >>> "
        + processName
        + " <<<\n"
        + "\n"
        + Util.sepOtherThreads
        + "\n"
        + trace
        + "\n"
        + Util.sepOtherThreadsEnding
        + "\n\n";
  }

  private String getTrace(String filepath, long anrTime) {

    // "\n\n----- pid %d at %04d-%02d-%02d %02d:%02d:%02d -----\n"
    // "Cmd line: %s\n"
    // "......"
    // "----- end %d -----\n"

    BufferedReader br = null;
    String line;
    Matcher matcher;
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
    StringBuilder sb = new StringBuilder();
    boolean found = false;

    try {
      br = new BufferedReader(new FileReader(filepath));
      while ((line = br.readLine()) != null) {
        if (!found && line.startsWith("----- pid ")) {

          // check current line for PID and log time
          matcher = patPidTime.matcher(line);
          if (!matcher.find() || matcher.groupCount() != 2) {
            continue;
          }
          String sPid = matcher.group(1);
          String sLogTime = matcher.group(2);
          if (sPid == null || sLogTime == null) {
            continue;
          }
          if (pid != Integer.parseInt(sPid)) {
            continue; // check PID
          }
          Date dLogTime = dateFormat.parse(sLogTime);
          if (dLogTime == null) {
            continue;
          }
          long logTime = dLogTime.getTime();
          if (Math.abs(logTime - anrTime) > anrTimeoutMs) {
            continue; // check log time
          }

          // check next line for process name
          line = br.readLine();
          if (line == null) {
            break;
          }
          matcher = patProcessName.matcher(line);
          if (!matcher.find() || matcher.groupCount() != 1) {
            continue;
          }
          String pName = matcher.group(1);
          if (pName == null || !(pName.equals(this.processName))) {
            continue; // check process name
          }

          found = true;

          sb.append(line).append('\n');
          sb.append("Mode: Watching /data/anr/*\n");

          continue;
        }

        if (found) {
          if (line.startsWith("----- end ")) {
            break;
          } else {
            sb.append(line).append('\n');
          }
        }
      }
      return sb.toString();
    } catch (Exception ignored) {
      return null;
    } finally {
      if (br != null) {
        try {
          br.close();
        } catch (Exception ignored) {
        }
      }
    }
  }
}
